#!/usr/bin/python
'''
Mulitprocess version of client_stub
'''
import socket
import struct
import select
import os
import sys
import logging
import thread
import traceback
import random
import string
import time
from Queue import Queue
from Queue import Empty
#we need just one connection for ssh, all other connection is combined to ssh tunnel
#only support NO AUTHENTICATION(X'00')
#only support tcp
#only support CONNECT(X'02') CMD
#only support connect to ip address

DEFAULT_SERVER = 'sevenever.vicp.net'
GET_IP_BY_HTTP_URL = 'http://lucky.sevenever.com/ip'
GET_IP_BY_HTTP_URL = 'http://lucy7.appspot.com/ip'
GET_IP_BY_HTTP_URL_USER = 'showmydebianip'
GET_IP_BY_HTTP_URL_PASS = ''
SERVER=DEFAULT_SERVER
SERVERPORT=80
PORT=34777
PIDFILE='htclientpid'
LOGFILE='htclientlog'
key='Zmpsd28AXDkyMzA5NDjvdzl3'

class ConnectClosed(Exception):pass

log=logging.info
#log=lambda x: None

def read(sk, cnt):
    buf=''
    while len(buf)<cnt:
        tp = sk.recv(cnt-len(buf))
        if not tp:
            raise ConnectClosed
        buf += tp
    return buf

def readline(sk):
    buf=''
    c=None
    while c!='\n':
        c = sk.recv(1)
        if not c:
            raise ConnectClosed
        buf += c
    return buf

def run(port):
    global queue
    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sk.bind(('127.0.0.1', port))
    sk.listen(10)
    log('Listener pid: %d listening on port %d' % (os.getpid(), port))
    queue = Queue()
    thread.start_new_thread(TrafficStatisticsThread, ())
    while True:
        try:
            cs, client = sk.accept()
        except Exception,e:
            log('Listener Error while accept: %s' % e)
            sk.shutdown()
            sk.close()
            sys.exit(1)
        log('client %s:%d connected.' % client)
        thread.start_new_thread(threadfunc, (cs, client))

def TrafficStatisticsThread():
    global queue
    
    try:
        upcount = 0
        downcount = 0
        while True:
            time.sleep(60)
            while True:
                try:
                    item = queue.get(False)
                    upcount += item[0]
                    downcount += item[1]
                except Empty:
                    break
            log('Total traffic statistics: up (%d) BYTES, down (%d) BYTES' % (upcount, downcount) )
    except Exception,e:
        log('Traffic statistics thread exception catched: %s' % traceback.format_exc())
    

def threadfunc(cs, client):
    global SERVER
    global PORT
    global queue
    up = 0
    down = 0
    upoob = 0
    downoob = 0
    upcount = 0
    downcount = 0
    upoobcount = 0
    downoobcount = 0
    
    ss = None
    try:
        # recv ver and method count
        buf = read(cs, 2)
        ver, mtd_cnt = struct.unpack('!BB', buf[:2])
        log('client %s:%d use protocl version: %d' % (client[0], client[1], ver))
        if ver != 5:
            log('client %s:%d protocol version unsupported.' % client)
            cs.close()
            return
        buf = read(cs, mtd_cnt)
        log('client %s:%d supported auth method: %s' % (client[0], client[1], repr(buf)))
        for mtd in buf:
            (mtd,) = struct.unpack('!B', mtd)
            if mtd == 0:
                cs.send('\x05\x00')
                break
        else:
            log('client %s:%d all auth method unsupported.' % client)
            cs.send('\x05\xFF')
            cs.close()
            return
        buf = read(cs, 4)
        ver, cmd, rsv, atyp = struct.unpack('!BBBB', buf)
        log('client %s:%d CMD:%d, atyp:%d' % (client[0], client[1], cmd, atyp))
        if cmd != 1:
            log('client %s:%d unsupported CMD:%d' % (client[1], client[1], cmd))
            cs.send('\x05\x07\x00\x01\xFF\xFF\xFF\xFF\x00\x00')
            cs.close()
            return
        if atyp == 1:
            buf = read(cs, 4+2)
            addr = struct.unpack('!IH', buf)
            addr = (socket.inet_ntoa(struct.pack('L',socket.htonl(addr[0]))), addr[1])
        elif atyp == 3:
            buf = read(cs, 1)
            (addrlen,) = struct.unpack('!B', buf)
            log('client %s:%d connect dst addr lenght:%d' % (client[0], client[1], addrlen))
            buf = read(cs, addrlen+2)
            addr = struct.unpack('!%dsH' % addrlen, buf)
        else:
            log('client %s:%d unsupported address type:%d' % (client[0], client[1], atyp))
            cs.send('\x05\x08\x00\x01\xFF\xFF\xFF\xFF\x00\x00')
            cs.close()
            return

        log('client %s:%d request connect to %s:%d' % (client[0], client[1], addr[0], addr[1]))
        #connect to server
        ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # if is default server, use GET_IP_BY_HTTP_URL instead of DNS system
        log('SERVER is %s'%SERVER)
        tunnel_server=SERVER
        if DEFAULT_SERVER == tunnel_server:
            log('server is default server, use %s instead of DNS system'%GET_IP_BY_HTTP_URL)
            try:
                import urllib2,base64
                req=urllib2.Request(GET_IP_BY_HTTP_URL)
                req.add_header('Authorization', 'Basic ' + \
                        base64.encodestring('%s:%s'%(GET_IP_BY_HTTP_URL_USER,GET_IP_BY_HTTP_URL_PASS)))
                resp=urllib2.urlopen(req).read()
                log('got IP json data [[[%s]]]'%resp)
                try:
                    import json
                except:
                    log('json is not supported, parse response by ourselves')
                    raise Exception('parse json by self is not implemented, plz use python >= 2.6')
                jsonobj = json.loads(resp)
                if jsonobj['error'] != u'True':
                    tunnel_server = str(jsonobj['ip'])
                    log('tunnel_server is set to ip: %s'%tunnel_server)
            except Exception,e:
                #raise e
                log('exception while get ip by %s, using DNS system'%GET_IP_BY_HTTP_URL)
        
        ss.connect((tunnel_server, SERVERPORT))
        log('client %s:%d connected to httptunnel server %s:%d' % (client[0], client[1], tunnel_server, SERVERPORT))
        # send a connect request to connect first
        ss.send('\r\n'.join(['GET /%d/%s/%s HTTP/1.1' % (addr[1], key, ''.join( random.sample(string.letters+string.digits, 12))),
                             'Host: %s' % addr[0],
                             'Connection: keep-alive',
                             ])
                + '\r\n\r\n')
        # wait for server response
        line = readline(ss)
        if line.split(' ')[1] != '200':
            ss.close()
            cs.close()
            raise Exception('Fail to initialize connection')
        while line != '\r\n':
            line = readline(ss)
        log('client %s:%d connected to %s:%d' % (client[0], client[1], addr[0], addr[1]))
        
        cs.send('\x05\x00\x00\x01\xFF\xFF\xFF\xFF\x00\x00')
        #exchange data
        pkgcnt = 0
        while True:
            if pkgcnt >= 128:
                pkgcnt = 0
                queue.put((up+upoob, down+downoob))
                upcount += up
                upoobcount += upoob
                downcount += down
                downoobcount += downoob
                up = 0
                down = 0
                upoob = 0
                downoob = 0
                log('Traffic statistics in client %s:%d connection: up (%d) upoob (%d), down (%d) downoob (%d) BYTES' % (client[0], client[1],upcount, upoobcount, downcount, downoobcount) )
            rss,wss,xss = select.select([cs,ss],[],[cs,ss])
            pkgcnt += 1
            if cs in xss:
                c = cs.recv(1, socket.MSG_OOB)
                if not c:
                    ss.close()
                    cs.close()
                    raise ConnectClosed('eof while recv oob data from client')
                ss.send(c, socket.MSG_OOB)
                upoob += 1
            if ss in xss:
                c = ss.recv(1, socket.MSG_OOB)
                if not c:
                    ss.close()
                    cs.close()
                    raise ConnectClosed('eof while recv oob data from server')
                cs.send(c, socket.MSG_OOB)
                downoob += 1
            if cs in rss:
                rs = cs
                ws = ss
                buf = rs.recv(4096)
                if not buf:
                    ss.close()
                    cs.close()
                    raise ConnectClosed('eof while recv data from client')
                ws.send(buf)
                up += len(buf)
            if ss in rss:
                rs = ss
                ws = cs
                buf = rs.recv(4096)
                if not buf:
                    ss.close()
                    cs.close()
                    raise ConnectClosed('eof while recv data from server')
                ws.send(buf)
                down += len(buf)
    except ConnectClosed,e:
        log('client %s:%d closed. %s ' % (client[0], client[1], e))
    except Exception,e:
        log('client %s:%d exception catched: %s' % (client[0], client[1], traceback.format_exc()))
    finally:
        try:
            ss.close()
        except:
            pass
        try:
            cs.close()
        except:
            pass
        upcount += up
        upoobcount += upoob
        downcount += down
        downoobcount += downoob
        queue.put((up+upoob, down+downoob))
        log('Final traffic statistics of client %s:%d connection: up (%d) upoob (%d), down (%d) downoob (%d) BYTES' % (client[0], client[1],upcount, upoobcount, downcount, downoobcount) )
def daemonize():
    if hasattr(os, 'fork'):
        if os.fork():
            sys.exit(0)
        os.setsid()
        #fork again
        if os.fork():
            sys.exit(0)
        os.chdir('/')
        os.umask(0)
    sys.stdin.close()
    sys.stdout.close()
    sys.stderr.close()
def main():
    global SERVER
    global PORT
    global PIDFILE
    global LOGFILE
    
    try:
        if len(sys.argv) > 1:
            SERVER = sys.argv[1]
            PORT = int(sys.argv[2])
            PIDFILE = PIDFILE + str(PORT)
            LOGFILE = LOGFILE + str(PORT)
        try:
            open(LOGFILE, 'a')
        except:
            sys.stderr.write('cannot open log file: %s\n' % LOGFILE)
            return 1
        daemonize()
        logging.basicConfig(level=logging.DEBUG,
                            format='%(asctime)s %(levelname)s %(message)s',
                            filename=LOGFILE)
        open(PIDFILE,'w').write('%d' % os.getpid())
        run(PORT)
        
        return 0
    except:
        log('exception catched: %s' % traceback.format_exc())
        return 1

if __name__ == '__main__':
    sys.exit(main())

